"""
解析日期时间对象的函数
"""

# We're using regular expressions rather than time.strptime because:
# - They provide both validation and parsing.
# - They're more flexible for datetimes.
# - The date/datetime/time constructors produce friendlier error messages.

import re

import pytz

from my_typing import *

utc = pytz.utc

date_re = re.compile(
    r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$"
)

time_re = re.compile(
    r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
    r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
)

datetime_re = re.compile(
    r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
    r"[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
    r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
    r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
)

standard_duration_re = re.compile(
    r"^"
    r"(?:(?P<days>-?\d+) (days?, )?)?"
    r"(?P<sign>-?)"
    r"((?:(?P<hours>\d+):)(?=\d+:\d+))?"
    r"(?:(?P<minutes>\d+):)?"
    r"(?P<seconds>\d+)"
    r"(?:\.(?P<microseconds>\d{1,6})\d{0,6})?"
    r"$"
)

# Support the sections of ISO 8601 date representation that are accepted by
# timedelta
iso8601_duration_re = re.compile(
    r"^(?P<sign>[-+]?)"
    r"P"
    r"(?:(?P<days>\d+(.\d+)?)D)?"
    r"(?:T"
    r"(?:(?P<hours>\d+(.\d+)?)H)?"
    r"(?:(?P<minutes>\d+(.\d+)?)M)?"
    r"(?:(?P<seconds>\d+(.\d+)?)S)?"
    r")?"
    r"$"
)

# Support PostgreSQL's day-time interval format, e.g. "3 days 04:05:06". The
# year-month and mixed intervals cannot be converted to a timedelta and thus
# aren't accepted.
postgres_interval_re = re.compile(
    r"^"
    r"(?:(?P<days>-?\d+) (days? ?))?"
    r"(?:(?P<sign>[-+])?"
    r"(?P<hours>\d+):"
    r"(?P<minutes>\d\d):"
    r"(?P<seconds>\d\d)"
    r"(?:\.(?P<microseconds>\d{1,6}))?"
    r")?$"
)


def get_fixed_timezone(offset) -> timezone:
    """ 返回与 UTC 具有固定偏移量的 tzinfo 实例 """

    if isinstance(offset, timedelta):
        offset = offset.total_seconds() // 60

    sign = (
        "-"
        if offset < 0
        else "+"
    )
    hhmm = "%02d%02d" % divmod(abs(offset), 60)
    name = sign + hhmm

    return timezone(timedelta(minutes=offset), name)


def parse_date(value) -> DateOrNone:
    """
    解析字符串并返回 date

    如果输入格式正确但不是有效的日期，则引发 ValueError
    如果输入格式不正确，则返回 None
    """

    match = date_re.match(value)

    if match:
        kw = {
            k: int(v)
            for k, v in match.groupdict().items()
        }

        return date(**kw)


def parse_time(value) -> TimeOrNone:
    """
    解析字符串并返回 time

    该函数不支持时区偏移

    如果输入格式正确但不是有效的日期时间，则引发 ValueError
    如果输入格式不正确，特别是包含偏移量，则返回 None
    """

    match = time_re.match(value)

    if match:
        kw = match.groupdict()
        kw["microsecond"] = (
            kw["microsecond"]
            and kw["microsecond"].ljust(6, "0")
        )
        kw = {
            k: int(v)
            for k, v in kw.items()
            if v is not None
        }

        return time(**kw)


def parse_datetime(value) -> DateTimeOrNone:
    """
    解析字符串并返回 datetime

    该函数支持时区偏移
    当输入包含 1 时，输出使用与 UTC 具有固定偏移量的时区

    如果输入格式正确但不是有效的日期时间，则引发 ValueError
    如果输入格式不正确，则返回 None
    """

    match = datetime_re.match(value)

    if match:
        kw = match.groupdict()
        kw["microsecond"] = (
            kw["microsecond"]
            and kw["microsecond"].ljust(6, "0")
        )
        tzinfo = kw.pop("tzinfo")  # noqa

        if tzinfo == "Z":
            tzinfo = utc  # noqa

        elif tzinfo is not None:
            offset_mins = (  # noqa
                int(tzinfo[-2:])
                if len(tzinfo) > 3
                else 0
            )
            offset = 60 * int(tzinfo[1:3]) + offset_mins

            if tzinfo[0] == "-":
                offset = -offset

            tzinfo = get_fixed_timezone(offset)  # noqa

        kw = {
            k: int(v)
            for k, v in kw.items()
            if v is not None
        }

        kw["tzinfo"] = tzinfo  # noqa

        return datetime(**kw)
